package com.ian.config;

import org.springframework.context.annotation.Bean;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import static org.springframework.security.config.Customizer.withDefaults;

/**
 * Security Config
 * @author Witt
 * @version 1.0.0
 * @date 2022/5/7
 */
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests(requests -> requests
                        .antMatchers(
                                "/",
//                                "/*.html",
//                                "/**/*.html",
                                "/**/*.css",
                                "/**/*.js",
                                "/js/**",
                                "/css/**",
                                "/img/**",
                                "/test").permitAll()
                        .anyRequest().authenticated())
                .formLogin(formLogin -> formLogin.loginPage("/login").loginProcessingUrl("/doLogin").permitAll())
                /*
                1. The sample explicitly sets cookieHttpOnly=false. This is necessary to allow JavaScript (i.e. AngularJS) to read it.
                If you do not need the ability to read the cookie with JavaScript directly, it is recommended to omit cookieHttpOnly=false
                (by using new CookieCsrfTokenRepository() instead) to improve security.

                2. Spring Security can easily be configured to store the expected CSRF token in a cookie。
                    （Cookie中的键值对类似于这样：XSRF-TOKEN=6b808a0f-568d-44f4-84da-047f3a101c32）
                当csrf开启时，spring security会给每一个thymeleaf模板页面中的使用 “th:action="@{/upload}"” 语法的表单自动添加一个隐藏元素，例如：
                    <input type="hidden" name="_csrf" value="71f43a49-42b6-40fc-9981-8f015063daa9">
                    <input type="hidden" name="_csrf" value="4bedb888-33df-4c97-8d46-0f5bfdaa0456">
                    <input type="hidden" name="_csrf" value="3ecb1a5a-0308-4b1e-b7a1-5f7bc2af5f3d">
                 用于校验 csrf token，并且每次request请的 csrf token 都不一样，不需要我们手动添加，非常省事。
                 这是spring security和thymeleaf框架整合产生的效果，那么如果没有自动添加，但也需要csrf，怎么办？可以手动添加csrf token：
                 因为spring security会传递csrf属性到页面，所以在thymeleaf框架中，我们可以手动写上以下元素：
                    <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
                    在非thymeleaf框架中，比如前后端分离的系统，比如vue开发中的普通的html页面，我们就用普通的前后端分离传值的方式：
                        Spring Security框架会自动将token放在cookie中（XSRF-TOKEN），在发起post请求时，通过js从cookie中获取到token，然后在header中添加属性X-XSRF-TOKEN，
                        （参考 src/main/resources/static/test.html ），
                        至于为什么要在header中使用属性X-XSRF-TOKEN，直接看 CookieCsrfTokenRepository 的源码就知道了（这里使用了CookieCsrfTokenRepository）。

                 3. 适用场景：Our recommendation is to use CSRF protection for any request that could be processed by a browser by normal users.
                    不适用场景：a. If you are only creating a service that is used by non-browser clients, you will likely want to disable CSRF protection.
                              b. 不使用 Cookie/Session 认证的场合，比如 使用 token认证，JWT认证等。
                              c. 前后端分离的系统。因为前端和后端可能IP或端口不同，导致了前端向后端发起请求的话，
                                实际上总是跨域的，没办法使用csrf保护，因为如果启用csrf保护功能，前端就永远没办法向后端发起请求。

                 4. By default Spring Security stores the expected CSRF token in the HttpSession using HttpSessionCsrfTokenRepository.
                    This can lead to a situation where the session expires which means there is not an expected CSRF token to validate against.
                    So there can be cases where users will want to configure a custom CsrfTokenRepository. For example,
                    it might be desirable to persist the CsrfToken in a cookie to support a JavaScript based application.
                    下面的配置就是配置自定义的 CsrfTokenRepository 为 CookieCsrfTokenRepository

                    注意：csrf token存到HttpSession 或 Cookie 中，只是针对后端，和前端无关，前端默认都仅仅存储在 Cookie 中，
                    前端Cookie示例: remember-me=dXNlcjM6MTY1Mjc4NDc0ODc4MTozM2Q1OWVjZGQ3ZTZkZTA5NzU3ODkyZjgxNzI1ZDBlNg; JSESSIONID=A26425501EEC457ED145EB103E296D64; XSRF-TOKEN=9c2a2d8a-23b1-4db3-81ef-9fa9e9736791

                 5. csrf默认使用 LazyCsrfTokenRepository，其 delegate 变量值默认是 HttpSessionCsrfTokenRepository 对象。（证实了 第4点）
                    该处使用了装饰者模式，LazyCsrfTokenRepository 是装饰者，HttpSessionCsrfTokenRepository 是被装饰者，
                    装饰者动态地将功能附加到被装饰者对象上，扩展了被装饰对象的功能。

                 6. 即使某个uri被设置为permitAll（如.antMatchers("/test").permitAll()），如果没有配置好csrf token，一样禁止访问，报403错误
                 */
                .csrf(csrf -> csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()))
                // .csrf(csrf -> csrf.disable()) // disable csrf functionality
                // 注销表单（/logout post请求）中，spring security渲染该注销表单时，自动添加了hidden element“<input type="hidden" name="_csrf" value="7a724090-90ee-4732-b5b4-6d3c7542d080">”
                // logout默认使用 AntPathRequestMatcher 的 matches() 方法 来 匹配 请求的uri和请求方法（默认为POST）
                .logout(logout -> logout
                        // this will perform logout with the URL /logout is requested with any HTTP method，亲测，可以用GET方法注销
                        // .logoutRequestMatcher(new AntPathRequestMatcher("/logout")))
                        .logoutUrl("/logout").logoutSuccessUrl("/login").permitAll())
        ;
    }

    @Override
    @Bean
    protected UserDetailsService userDetailsService() {
        UserDetails user = User.withUsername("user").password("123").roles("user").passwordEncoder(pw -> "{bcrypt}" + new BCryptPasswordEncoder().encode(pw)).build();
        // UserDetails user = User.withUsername("user").password("{bcrypt}$2a$10$lmURdwCsjA9jFc5NgzX1Keq5O91WEuOOgEtMQ246X.G/cu.HSqMTG").roles("user").build();
        UserDetails admin = User.withUsername("admin").password("123").roles("admin").passwordEncoder(pw -> "{bcrypt}" + new BCryptPasswordEncoder().encode(pw)).build();
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(user);
        manager.createUser(admin);
        return manager;
    }
}
